﻿#region Header

// Majesty of Omega - The next evolution of macro management space strategy
// 
// Copyright 2007-2009 André Claaßen
// 
// This file is protected by the terms and conditions of the
// Microsoft Community License (Ms-CL 1.1), a copy of which should
// have been distributed along with this software. If not,
// you may find the license information at the following URL:
// 
// http://www.microsoft.com/resources/sharedsource/licensingbasics/communitylicense.mspx

#endregion

#region

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using DesignByContract;
using Majesty_of_Omega.Model.Game;
using Majesty_of_Omega.Util;
using Majesty_of_Omega.Config;

#endregion

namespace Majesty_of_Omega.Model.Research
{
    /// <summary>
    /// The in-game research manager. The research manager spreads the research points over the
    /// various technology schools (<see cref="TechSchool" />) and technology fields (<see cref="TechField" />)
    /// </summary>
    /// <remarks>
    /// The ResearchManager is Part of a <see cref="SaveGame" /> and will be stored and loaded from disk.
    /// </remarks>
    [Serializable]
    public class ResearchManager
    {
        /// <summary>
        /// The technology fields and the relative percentage values for spending research points
        /// </summary>
        private List<TechFieldResearch> techFieldResearchs = new List<TechFieldResearch>();

        /// <summary>
        /// The technology schools and the relative percentage values for spending research points
        /// </summary>
        private readonly List<TechSchoolResearch> techSchoolResearchs = new List<TechSchoolResearch>();

        /// <summary>
        /// List of applied technologies in the research queue
        /// </summary>
        private readonly List<AppliedResearchItem> techCurrentAppliedResearchs = new List<AppliedResearchItem>();

        /// <summary>
        /// List of applied technologies waiting to be researched
        /// </summary>
        private readonly List<AppliedResearchItem> techAvailableAppliedResearchs = new List<AppliedResearchItem>();

        /// <summary>
        /// List of all fully researched applied Technologies
        /// </summary>
        private readonly List<TechApplicationProxy> researchedTechApplications = new List<TechApplicationProxy>();

        /// <summary>
        /// Gets the researched tech applications.
        /// </summary>
        /// <value>The researched tech applications.</value>
        public List<TechApplicationProxy> ResearchedTechApplications
        {
            get
            {
                return researchedTechApplications;
            }
        }

        /// <summary>
        /// Gets the current applied research projects
        /// </summary>
        /// <remarks>
        /// This property gives a list of all funded Projects for applied research. 
        /// 
        /// From the design document:
        /// =========================
        /// Applied Research is even more automated from the player's perspective. 
        /// The various Leaders throughout the player's civilization will regularly 
        /// generate "needs" for new or improved technologies, and of course the 
        /// Military Command will constantly be seeking technological advantages 
        /// for its vessels and installations.
        /// 
        /// The player will see a queue of desired projects, prioritized based on 
        /// the requests from Leaders and the Military and by the empire-wide 
        /// settings that determine the percentage share of resources to be split 
        /// between military and civilian efforts. The player can examine the 
        /// queues and reorganize them, as appropriate, or delete items which 
        /// are not desirable.  Or, the player can simply let the AIs take care 
        /// of the details and focus on other parts of the game.
        ///
        /// </remarks>
        /// <value>The tech applied researchs.</value>
        public List<AppliedResearchItem> TechCurrentAppliedResearchProjects
        {
            get { return techCurrentAppliedResearchs; }
        }

        /// <summary>
        /// Gets the tech available applied researchs.
        /// </summary>
        /// <value>The tech available applied researchs.</value>
        public List<AppliedResearchItem> TechAvailableAppliedResearchs
        {
            get { return techAvailableAppliedResearchs; }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ResearchManager"/> class.
        /// </summary>
        public ResearchManager()
        {
            foreach (var techField in SpreadSheetManager.TechTables.TechFields)
            {
                techFieldResearchs.Add(new TechFieldResearch(this, techField));
            }

            foreach (var techSchool in SpreadSheetManager.TechTables.TechSchools)
            {

                var techSchoolResearch = new TechSchoolResearch(techSchool);
                var techFieldList = from techFieldResearch in techFieldResearchs
                                    where techFieldResearch.TechField.TechSchool == techSchool
                                    select techFieldResearch;
                techSchoolResearch.TechFieldResearchForSchool.AddRange(techFieldList);
                techSchoolResearchs.Add(techSchoolResearch);
            }
            AddDefaultTechnologies();
            BalanceResearchPoints(techSchoolResearchs);
        }


        /// <summary>
        /// Adds all technologies, which require no research (tech level 0)
        /// </summary>
        private void AddDefaultTechnologies()
        {
            researchedTechApplications.AddRange(
                    from techApplication in SpreadSheetManager.TechTables.TechApplications
                    where techApplication.IsDefaultTechnology
                    select new TechApplicationProxy(techApplication));
        }

        /// <summary>
        /// Balances the research points.
        /// </summary>
        /// <param name="researchItems">The research items.</param>
        public void BalanceResearchPoints(IEnumerable researchItems)
        {
            var fragment = 100;
            int count = 0;
            var sum = CalcSumResearchPercentage(researchItems, ref count);
            if (sum == 100) return;

            if (sum == 0)
            {
                // initialize research percentage equally
                foreach (var o in researchItems)
                {
                    ResearchItem item = (ResearchItem)o;
                    item.ResearchPercentage = 100 / count;
                    fragment -= item.ResearchPercentage;
                }
            }
            else
            {
                // rebalance the research points. the sum of all points isn't equal to 100
                foreach (var o in researchItems)
                {
                    ResearchItem item = (ResearchItem)o;
                    item.ResearchPercentage = item.ResearchPercentage * 100 / sum;
                    fragment -= item.ResearchPercentage;
                }
            }

            // spread the fragment over the remaining technologies
            IEnumerator enumerator = researchItems.GetEnumerator();
            for (int i = 0; i < Math.Abs(fragment); i++)
            {
                if (!enumerator.MoveNext())
                {
                    enumerator.Reset();
                    enumerator.MoveNext();
                }
                ResearchItem item = (ResearchItem)enumerator.Current;
                if (fragment > 0)
                {
                    item.ResearchPercentage++;
                }
                else
                {
                    item.ResearchPercentage--;
                }
            }
        }

        /// <summary>
        /// Calculate the total sum of research percentage values
        /// </summary>
        /// <param name="researchItems">The research items.</param>
        /// <param name="count">The count.</param>
        /// <returns></returns>
        private static int CalcSumResearchPercentage(IEnumerable researchItems, ref int count)
        {
            int sum = 0;
            foreach (var o in researchItems)
            {
                ResearchItem item = (ResearchItem)o;
                sum += item.ResearchPercentage;
                count++;
            }
            return sum;
        }

        /// <summary>
        /// Gets the tech school researchs.
        /// </summary>
        /// <value>The tech school researchs.</value>
        public List<TechSchoolResearch> TechSchoolResearchs
        {
            get
            {
                return techSchoolResearchs;
            }
        }

        /// <summary>
        /// Gets the tech field researchs.
        /// </summary>
        /// <remarks>
        /// The setter is only allowed for unit tests
        /// </remarks>
        /// <value>The tech field researchs.</value>
        public List<TechFieldResearch> TechFieldResearchs
        {
            get { return techFieldResearchs; }
            set { techFieldResearchs = value; }
        }

        #region Equality operators

        /// <summary>
        /// Equalses the specified obj.
        /// </summary>
        /// <param name="obj">The obj.</param>
        /// <returns></returns>
        public bool Equals(ResearchManager obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            if (ReferenceEquals(this, obj)) return true;
            bool equalsTFR = EqualHelper.AreCollectionsEqual(obj.techFieldResearchs, techFieldResearchs);
            bool equalsTSR = EqualHelper.AreCollectionsEqual(obj.techSchoolResearchs, techSchoolResearchs);
            return equalsTFR && equalsTSR;
        }

        /// <summary>
        /// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.
        /// </summary>
        /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
        /// <returns>
        /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
        /// </returns>
        /// <exception cref="T:System.NullReferenceException">
        /// The <paramref name="obj"/> parameter is null.
        /// </exception>
        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            if (ReferenceEquals(this, obj)) return true;
            if (obj.GetType() != typeof(ResearchManager)) return false;
            return Equals((ResearchManager)obj);
        }

        /// <summary>
        /// Serves as a hash function for a particular type.
        /// </summary>
        /// <returns>
        /// A hash code for the current <see cref="T:System.Object"/>.
        /// </returns>
        public override int GetHashCode()
        {
            unchecked
            {
                return ((techFieldResearchs != null ? techFieldResearchs.GetHashCode() : 0) * 397) ^
                       (techSchoolResearchs != null ? techSchoolResearchs.GetHashCode() : 0);
            }
        }

        #endregion

        /// <summary>
        /// Gets a list of researched technology fields for a given technology school.
        /// </summary>
        /// <remarks>
        /// Important: Only researched technology fields were given back.
        /// </remarks>
        /// <param name="school">The school.</param>
        /// <returns></returns>
        public IEnumerable<TechFieldResearch> GetTechFieldResearchForTechSchool(TechSchool school)
        {
            var techFieldList = from techFieldResearch in techFieldResearchs
                                where techFieldResearch.TechField.TechSchool == school &&
                                      techFieldResearch.Researched
                                select techFieldResearch;
            BalanceResearchPoints(techFieldList);
            return techFieldList;
        }

        /// <summary>
        /// Gets the unresearched tech fields.
        /// </summary>
        /// <returns></returns>
        public IEnumerable<TechFieldResearch> GetUnresearchedTechFields()
        {
            return from techFieldResearch in techFieldResearchs
                   where !techFieldResearch.Researched
                   select techFieldResearch;
        }

        /// <summary>
        /// Gets the next upcoming technologies with the heavy use of linq
        /// </summary>
        /// <param name="nextLevels">The next levels.</param>
        /// <returns></returns>
        public IEnumerable<TechApplication> GetNextUpcomingTechnologies(int nextLevels)
        {
            var upcomingTechnologies =
                from techFieldResearch in techFieldResearchs
                from techapp in techFieldResearch.TechField.TechApplications
                where !IsTechnologyKnown(techapp) && IsResearchable(techapp) && IsUpcomingTechLevel(techapp, nextLevels)
                orderby techapp.TechFieldDependencies.Min(tf => tf.TechLevel)
                select techapp;

            return upcomingTechnologies;
        }


        /// <summary>
        /// Gets the researched tech apps for field and level.
        /// </summary>
        /// <param name="field">The field.</param>
        /// <param name="level">The level.</param>
        /// <returns></returns>
        public IEnumerable<TechApplication> GetResearchedTechAppsForFieldAndLevel(TechField field, int level)
        {
            return from techApplication in ResearchedTechApplications
                   where
                       techApplication.TechApplication.DefaultTechFieldDependency.
                           TechField == field &&
                           level == techApplication.TechApplication.DefaultTechFieldDependency.TechLevel
                   select
                       techApplication.TechApplication;
        }

        /// <summary>
        /// Determines whether [is upcoming tech level] [the specified techapp].
        /// </summary>
        /// <param name="techapp">The techapp.</param>
        /// <param name="levels">The levels.</param>
        /// <returns>
        /// 	<c>true</c> if [is upcoming tech level] [the specified techapp]; otherwise, <c>false</c>.
        /// </returns>
        private bool IsUpcomingTechLevel(TechApplication techapp, int levels)
        {
            foreach (var fieldDependency in techapp.TechFieldDependencies)
            {
                var techFieldResearch = GetTechFieldResearch(fieldDependency.TechField);
                // if techlevel from the field dependency is to big, the tech is not
                // researchable
                if (fieldDependency.TechLevel > (techFieldResearch.TechLevel + levels) ||
                    !techFieldResearch.Researched)
                    return false;
            }
            return true;
        }

        /// <summary>
        /// Determines whether the specified techapp is researchable.
        /// A tech application is researchable, if all depending fields are 
        /// unlocked (researched)
        /// </summary>
        /// <param name="techapp">The techapp.</param>
        private bool IsResearchable(TechApplication techapp)
        {
            foreach (var fieldDependency in techapp.TechFieldDependencies)
            {
                var techFieldResearch = GetTechFieldResearch(fieldDependency.TechField);
                if (!techFieldResearch.Researched)
                    return false;
            }
            return true;
        }

        /// <summary>
        /// Gets the tech field research.
        /// </summary>
        /// <param name="field">The field.</param>
        /// <returns></returns>
        public TechFieldResearch GetTechFieldResearch(TechField field)
        {
            return techFieldResearchs.First(research => research.TechField == field);
        }

        /// <summary>
        /// Determines whether the specified techapp is known.
        /// </summary>
        /// <remarks>
        /// A technology is researched, if it is 
        /// <list type="bullet" >
        /// <item>a full researched technology</item>
        /// <item>an unresearched application technology</item>
        /// <item>a technology, which is currently under researched</item>
        /// </list>
        /// </remarks>
        /// <param name="techapp">The techapp.</param>
        /// <returns>
        /// 	<c>true</c> if the specified techapp is known; otherwise, <c>false</c>.
        /// </returns>
        private bool IsTechnologyKnown(TechApplication techapp)
        {
            if (researchedTechApplications.Any(ta => ta.TechApplication == techapp)) return true;

            if (techAvailableAppliedResearchs.Any(ta => ta.Technology == techapp)) return true;

            if (techCurrentAppliedResearchs.Any(ar => ar.Technology == techapp)) return true;

            return false;
        }

        /// <summary>
        /// Adds the research item to the list of current applied research projects.
        /// </summary>
        /// <param name="research">The research.</param>
        public void AddResearchToCurrentAppliedResearchProjects(AppliedResearchItem research)
        {
            Require.IsNotNull(research);
            Require.IsTrue(techAvailableAppliedResearchs.Contains(research), "The research item must be in the list of available applied research items");
            Require.IsFalse(techCurrentAppliedResearchs.Contains(research), "The research item should not be in the list of the current applied research items");
            techAvailableAppliedResearchs.Remove(research);
            techCurrentAppliedResearchs.Add(research);
        }

        /// <summary>
        /// Gets or sets the total research points for the actual turn
        /// </summary>
        /// <value>The total research points.</value>
        public int TotalResearchPointsOfCurrentTurn { get; set; }

        /// <summary>
        /// Retrieve the sum of distributable applied research points.
        /// </summary>
        /// <value>The applied research points.</value>
        public int SumOfAppliedResearchPoints
        {
            get
            {
                int sumOfAppliedResearch = techCurrentAppliedResearchs.Sum(appliedResearchItem => appliedResearchItem.TurnCost);
                if (sumOfAppliedResearch > TotalResearchPointsOfCurrentTurn)
                    sumOfAppliedResearch = TotalResearchPointsOfCurrentTurn;
                return sumOfAppliedResearch;
            }
        }

        /// <summary>
        /// Retrieve the research points for fundamental research
        /// </summary>
        /// <value>The fundamental research points.</value>
        public int SumOfFundamentalResearchPoints
        {
            get
            {
                int fundResearchPoints = TotalResearchPointsOfCurrentTurn - SumOfAppliedResearchPoints;
                Ensure.IsTrue(fundResearchPoints >= 0);
                return fundResearchPoints;
            }
        }

        /// <summary>
        /// Gets the tech field research by type id
        /// </summary>
        /// <param name="typeId">The type id.</param>
        public TechFieldResearch GetTechFieldResearch(string typeId)
        {
            TechField techField = SpreadSheetManager.TechTables.TechFieldDictionary[typeId];
            return GetTechFieldResearch(techField);
        }

        /// <summary>
        /// Unlocks the new Technology Field.
        /// </summary>
        /// <remarks>
        /// Sometimes, a new technology field will be discovered, if all dependendet technology 
        /// fields are research by a given level. This behavior is configured in the 
        /// TechField.Field_Dependencies property.
        /// When a new technology field is discovered, it gains a default research percentage,
        /// which will be subtracted for all other technology fields of tech school.
        /// </remarks>
        /// <param name="techFieldResearchToUnlock">The tech field research.</param>
        public void UnlockTechField(TechFieldResearch techFieldResearchToUnlock)
        {
            techFieldResearchToUnlock.UnlockTechField();

            var techResearchsForTechSchool =
                GetTechFieldResearchForTechSchool(techFieldResearchToUnlock.TechField.TechSchool);

            // Calculate the default research for the new technology field
            techFieldResearchToUnlock.ResearchPercentage = 100 / techResearchsForTechSchool.Count();

            int nReducedResearch = techFieldResearchToUnlock.ResearchPercentage;
            foreach (var fieldResearch in techResearchsForTechSchool)
            {
                if (nReducedResearch == 0) break; // abort: no more research to spread
                if (fieldResearch == techFieldResearchToUnlock) continue;
                int n = fieldResearch.ResearchPercentage * techFieldResearchToUnlock.ResearchPercentage / 100;
                if (nReducedResearch < n)
                    n = nReducedResearch;

                fieldResearch.ResearchPercentage -= n;
                nReducedResearch -= n;
            }
        }

        /// <summary>
        /// Creates a new available research for the given technology
        /// </summary>
        /// <param name="techApplication">The technology.</param>
        public void CreateNewAvailableResearch(TechApplication techApplication)
        {
            Require.IsFalse(techAvailableAppliedResearchs.Any(ar => ar.Technology == techApplication),
               "The tech application is already an applied research item");
            techAvailableAppliedResearchs.Add(new AppliedResearchItem(techApplication));
        }

        /// <summary>
        /// Starts a project of applied research.
        /// </summary>
        /// <param name="availableResearchItem">The available research item.</param>
        /// <param name="fundingLevel">Funding level of the research</param>
        public void StartAppliedResearchProject(AppliedResearchItem availableResearchItem, FundingLevel fundingLevel)
        {
            bool removed = TechAvailableAppliedResearchs.Remove(availableResearchItem);
            Require.IsTrue(removed, "You can only start a research on a applied technology, " +
                                    "which is in the list ov available applied research");

            TechCurrentAppliedResearchProjects.Add(availableResearchItem);
            availableResearchItem.BudgetFundingLevel = fundingLevel;
            availableResearchItem.PercentageResearchPerTurn =
                SpreadSheetManager.ResearchConfiguration.GetRandomAppliedResearchPercentagePerTurn();

            Ensure.IsTrue(availableResearchItem.PercentageResearchPerTurn > 0);
            Ensure.IsTrue(availableResearchItem.TurnsRemaining > 0);
        }

        /// <summary>
        /// This function takes a applied research item out of the project state into the list
        /// of fully researched technologies.
        /// </summary>
        /// <param name="item">The item.</param>
        public void ApplyFullyResearchedTechnology(AppliedResearchItem item)
        {
            Require.IsTrue(item.IsFullyResearched);
            Require.IsTrue(TechCurrentAppliedResearchProjects.Contains(item));

            TechCurrentAppliedResearchProjects.Remove(item);
            ResearchedTechApplications.Add(new TechApplicationProxy(item.Technology));
        }

        /// <summary>
        /// Gets the researched tech fields for a tech school.
        /// </summary>
        /// <param name="techSchoolTypeID">The tech school type ID.</param>
        /// <returns></returns>
        public List<TechFieldResearch> GetResearchedTechFieldsForSchool(string techSchoolTypeID)
        {
            var techSchoolResearch = GetTechSchoolResearch(techSchoolTypeID);
            return new List<TechFieldResearch>(techSchoolResearch.GetResearchedTechFields());
        }

        /// <summary>
        /// Gets the tech school research.
        /// </summary>
        /// <param name="techSchoolTypeID">The tech school type ID.</param>
        /// <returns></returns>
        public TechSchoolResearch GetTechSchoolResearch(string techSchoolTypeID)
        {
            var techSchoolResearch = techSchoolResearchs.Find(tsr => tsr.TechSchool.Type_ID == techSchoolTypeID);
            Ensure.IsNotNull(techSchoolResearch);
            return techSchoolResearch;
        }

        /// <summary>
        /// This type determines, which Tech application state infos the function <see cref="ResearchManager.GetTechApplicationStateForFieldAndLevel"/>
        /// returns
        /// </summary>
        public enum TechApplicationState
        {
            /// <summary>
            /// Get all tech applications: researched, available or unknown tech
            /// </summary>
            GetAllTechApps,
            /// <summary>
            /// Get only researched and available techs
            /// </summary>
            GetAvailableTechApps
        } ;

        /// <summary>
        /// Gets the tech application state for field and level.
        /// </summary>
        /// <param name="field">The field.</param>
        /// <param name="level">The level.</param>
        /// <param name="applicationState">Types of tech applications, which will be retrieved</param>
        /// <returns></returns>
        public List<TechApplicationStateInfo> GetTechApplicationStateForFieldAndLevel(TechField field, int level, TechApplicationState applicationState)
        {
            var stateInfos = new List<TechApplicationStateInfo>();

            var researchTechApps = from applicationProxy in ResearchedTechApplications
                                   where
                                       applicationProxy.TechApplication.DefaultTechFieldDependency.
                                           TechField == field &&
                                           level == applicationProxy.TechApplication.DefaultTechFieldDependency.TechLevel
                                   select
                                       new TechApplicationStateInfo(ResearchState.Researched, applicationProxy.TechApplication);
            stateInfos.AddRange(researchTechApps);

            var availableTechApps = from appliedResearchItem in this.TechAvailableAppliedResearchs
                                    where
                                         appliedResearchItem.Technology.DefaultTechFieldDependency.
                                            TechField == field &&
                                            level == appliedResearchItem.Technology.DefaultTechFieldDependency.TechLevel
                                    select
                                        new TechApplicationStateInfo(ResearchState.Researched, appliedResearchItem.Technology);
            stateInfos.AddRange(availableTechApps);

            if (applicationState == TechApplicationState.GetAllTechApps)
            {
                var unknownTechApps =
                    from techApplication in
                        SpreadSheetManager.TechTables.TechFieldDictionary[field.Type_ID].TechApplications
                    where
                        techApplication.DefaultTechFieldDependency.TechLevel == level &&
                        !researchTechApps.Any(
                             researchedTech => researchedTech.TechApplication.Type_ID == techApplication.Type_ID) &&
                        !availableTechApps.Any(
                             availableTech => availableTech.TechApplication.Type_ID == techApplication.Type_ID)
                    select
                        new TechApplicationStateInfo(ResearchState.Unknown, techApplication);
                stateInfos.AddRange(unknownTechApps);
            }

            return stateInfos;
        }
    }
}